// <copyright file="SelfIssuedSamlAuthSTS.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-16</date>
// <summary>Implements an Security Token Service for self issued backed cards.</summary>

namespace SharpSTS
{
    using System;
    using System.IdentityModel.Claims;
    using System.IdentityModel.Policy;
    using System.Security.Cryptography;    
    using System.ServiceModel;

    using Providers;

    /// <summary>
    /// Implements an Security Token Service for self issued backed cards.
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage(
        "Microsoft.Naming", 
        "CA1705:LongAcronymsShouldBePascalCased",
        Justification = "STS is neither long, or an acronym.")]
    [ServiceBehavior(Namespace = Constants.WSTrust.Namespace.Uri)]
    internal sealed class SelfIssuedSamlAuthSTS : WsTrustResponder
    {
        /// <summary>
        /// Validate the specified card identifier and version against the endpoint specific authentication information.
        /// </summary>
        /// <param name="cardId">The card identitifer for the current request.</param>
        /// <param name="cardVersion">The card version for the current request.</param>
        /// <returns>
        /// True if the current authenticated user is authorised to use the specified card identitifer and version.
        /// </returns>
        /// <remarks>An STS instance must implement a suitable authorization process customised to their authentication strategy.</remarks>
        protected override bool AuthoriseCardUse(string cardId, int cardVersion)
        {
            Claim ppid = null;
            
            // Get the public key hash sent as part of the authorisation headers.
            string publicKeyHash =
                GetIssuerPubKeyHash(OperationContext.Current.ServiceSecurityContext.AuthorizationContext);

            // Get the PPID sent for the self issued card used to back the managed card.
            foreach (ClaimSet cs in OperationContext.Current.ServiceSecurityContext.AuthorizationContext.ClaimSets)
            {
                foreach (Claim claim in cs.FindClaims(ClaimTypes.PPID, Rights.PossessProperty))
                {
                    ppid = claim;
                }
            }

            // If we don't have a PPID or we couldn't get the public key hash we cannot authenticate, so return false.
            if (null == ppid || string.IsNullOrEmpty(publicKeyHash))
            {
                return false;
            }

            // Otherwise continue authentication via the policy provider.
            return AuthorisationPolicyManager.AuthorisedToUseCardId(ppid, publicKeyHash, cardId, cardVersion);
        }

        /// <summary>
        /// Gets the user identifier from card identifier.
        /// </summary>
        /// <param name="cardID">The card ID.</param>
        /// <returns>A user identifier.</returns>
        protected override object GetUserIdentifierFromCardId(string cardID)
        {
            return AuthorisationPolicyManager.GetUserIdFromCardId(cardID, AuthenticationType.SelfIssued);
        }

        #region Public Key Hashing Methods
        /// <summary>
        /// Gets the Unique RSA Claim from the SAML token.
        /// </summary>
        /// <param name="claimSet">the claimset which contains the PPID Claim</param>
        /// <returns>a RSA claim</returns>
        private static Claim GetUniqueRsaClaim(ClaimSet claimSet)
        {
            Claim rsa = null;

            foreach (Claim claim in claimSet.FindClaims(ClaimTypes.Rsa, Rights.PossessProperty))
            {
                if (null == rsa)
                {
                    rsa = claim;
                }
                else if (!rsa.Equals(claim))
                {
                    // Found two non-equal RSA claims
                    return null;
                }
            }

            return rsa;
        }

        /// <summary>
        /// Generates a UniqueID based off the Issuer's key
        /// </summary>
        /// <param name="authorizationContext">the Authorization Context</param>
        /// <returns>the hash of the public key of the issuer</returns>
        private static string GetIssuerPubKeyHash(AuthorizationContext authorizationContext)
        {
            foreach (ClaimSet cs in authorizationContext.ClaimSets)
            {
                Claim currentIssuerClaim = GetUniqueRsaClaim(cs.Issuer);

                if (currentIssuerClaim == null)
                {
                    continue;
                }
                
                RSA rsa = currentIssuerClaim.Resource as RSA;
                return null == rsa ? null : ComputeCombinedId(rsa);
            }

            return null;
        }

        /// <summary>
        /// Does the actual calculation of a combined ID from a value and an RSA key.
        /// </summary>
        /// <param name="issuerKey">The key of the issuer of the token</param>
        /// <returns>A computed has of the issuer key</returns>
        private static string ComputeCombinedId(RSA issuerKey)
        {
            RSAParameters rsaParams = issuerKey.ExportParameters(false);

            int index = 0;
            byte[] shaInput = new byte[rsaParams.Modulus.Length + rsaParams.Exponent.Length];
            byte[] shaOutput;

            rsaParams.Modulus.CopyTo(shaInput, index);
            index += rsaParams.Modulus.Length;
            rsaParams.Exponent.CopyTo(shaInput, index);

            using (SHA256 sha = SHA256.Create())
            {
                shaOutput = sha.ComputeHash(shaInput);
            }

            return Convert.ToBase64String(shaOutput);
        }
        #endregion
    }
}
